import os
import md5
import sys
import time
import socket
import irclib
import getopt
import logging
import logging.handlers
from datetime import datetime
import locale
import turbogears

turbogears.update_config(configfile="dev.cfg",
    modulename="arkivo.config")

from arkivo.model import *


class ArkivoClient:
    """A not-so-simple multiple server IRC client class.
    
    Based on the SimpleIRCClient class in irclib, but with extras.
    
    Doesn't deal with DCC in the slightest."""
    
    def __init__(self):
        self.ircobj = irclib.IRC()
        self.connections = {}
        self.connection_details = {}
        self.ircobj.add_global_handler("all_events", self._dispatcher, -10)
        self.current_services = []
        
    def _dispatcher(self, connection, event):
        """Dispatches events to the handler of the same event name (with a prefix of 'on_')"""
        handler = "on_%s" % event.eventtype()
        if hasattr(self, handler):
            getattr(self, handler)(connection, event)
            
    def create_connection(self, service, server, port, nick, password=None, username=None,
                ircname=None, localaddress="", localport=0):
        """Add a service to the connection dict for later use.
        Defunct now I'm pulling everything from the DB"""
        if not service in self.connections.keys():
            self.connection_details[service] = dict(server=server, port=port, nickname=nick,
                                            password=password, username=username,
                                            ircname=ircname, localaddress=localaddress,
                                            localport=0)
            self.connections[service] = self.ircobj.server()
                                            
    def remove_connection(self, service):
        pass
        
    def get_service_name(self, connection):
        """Retreives a service name for a given connection"""
        for k,v in self.connections.items():
            if v == connection:
                return k
        return None
                                            
    def connect_service(self, service):
        """Connect/reconnect a particular service"""
        try:
            db_service = Service.byName(service)
            d = db_service.connection_dict
            self.connections[service] = self.ircobj.server()
            self.connections[service].connect(**d)
            self.current_services.append(service)
        except SQLObjectNotFound:
            pass
                        
    def connect(self):
        """Connect/reconnect to all available services"""
        for service in Service.select():
            self.connect_service(service.name)
            
    def start(self):
        """Start the client"""
        self.ircobj.process_forever()
        
        
class ArkivoBot(ArkivoClient):
    
    def __init__(self):
        ArkivoClient.__init__(self)
        self.inmotd = dict()
        self.alt_nick = ''
        ident = md5.new('arkivobot md5 salt')
        ident.update(str(time.time()))
        self.ident = ident.hexdigest()
        logging.info("IDENT %s" % self.ident)
        self.last_check = {}
        self.delay = 30
        self.alternate = ''
        
        self.shutdown_init = False
        
        self.connection_channels = {}
        self.current_channels = {}
        
    def connect(self):
        try:
            ArkivoClient.connect(self)
            try:
                Setting.byName("is_online").data = True
            except SQLObjectNotFound:
                Setting(name="is_online", data=True)
            return True
        except irclib.ServerConnectionError:
            logging.error('Could not connect to one or more services')
            return False
            
    def shutdown(self):
        self.shutdown_init = True
        for service in [s.name for s in Service.select()]:
            connection = self.connections[service]
            try:
                connection.quit("Bot shutting down.  Bye bye!")
            except irclib.ServerNotConnectedError:
                pass
        Setting.byName("is_online").data = False
            
    def latest_channels(self, service):
        service = Service.byName(service)
        channels = Channel.select(AND(Channel.q.serviceID==service.id, Channel.q.enabled==True))
        return [chan.name for chan in channels]
        
    def join_channels(self, service, channels):
        if service not in self.current_channels.keys():
            self.current_channels[service] = []
            
        self.current_channels[service] = channels
            
        db_service = Service.byName(service)
        self.connection_channels[service] = self.latest_channels(service)
        logging.debug("connection_channels for %s: %s" % (service, self.connection_channels[service]))
        logging.debug("current_channels for %s: %s" % (service, channels))
        for c in channels:
           if c not in self.connection_channels[service]:
               logging.info('PARTing removed channel %s on %s' % (c, service))
               self.connections[service].part('#'+c, "(http://irclog.turbogears.org)")
        
        for c in self.connection_channels[service]:
           if c not in channels:
               logging.info('JOINing missing channel %s on %s' % (c, service))
               self.connections[service].join('#'+c)
               

        self.last_check[service] = time.time()
    
    def check_online(self, connection):
        """Does a little online check"""
        connection.privmsg(connection.get_nickname(), self.ident)
        connection.whois([connection.get_nickname()])
        #check for new services
        for service in [s.name for s in Service.select()]:
            if service not in self.current_services: self.connect_service(service)

        connection.execute_delayed(self.delay, self.check_online, [connection])
       
    def check_nick(self, connection):
        service = self.get_service_name(connection)
        nick = Service.byName(service).nickname
        if connection.get_nickname() != nick:
            connection.ison([nick])
        
    ###
    # Event handlers
    ###
    def on_ison(self, connect, event):
        service = self.get_service_name(connect)
        nick = Service.byName(service).nickname
        if nick not in [n.strip() for n in event.arguments()]:
            logging.warn('%s::Nick %s now available', nick)
            self.alternate = ''
            connect.nick(nick)
            
    def on_nicknameinuse(self, connect, event):
        service = self.get_service_name(connect)
        nick = Service.byName(service).nickname
        self.alternate += '_'
        connect.nick(nick+self.alternate)
        
    def on_whoischannels(self, connect, event):
        service = self.get_service_name(connect)
        channels = [c[1:].strip() for c in event.arguments()[1].split() if c.startswith('#')]
        self.join_channels(service, channels)
        
    def on_whoisuser(self, connect, event):
        service = self.get_service_name(connect)
        logging.debug("%s::WHOISUSER %s" % (service, event.target()))
        if service not in self.last_check.keys():
            self.last_check[service] = 0
        if time.time() > self.last_check[service] + 3*self.delay:
            logging.warn('Refreshing channel info for %s' % service)
            self.join_channels(service, [])
            
    def on_motd(self, connect, event):
        service = self.get_service_name(connect)
        if service not in self.inmotd.keys():
            self.inmotd[service] = False
        if not self.inmotd[service]:
            logging.info('%s::MOTD started' % service)
            self.inmotd[service] = True

    def on_endofmotd(self, connect, event):
        service = self.get_service_name(connect)
        self.inmotd[service] = False
        logging.info('%s::MOTD ended, join channels...' % service)
        connect.ison([Service.byName(service).nickname])
        self.check_online(connect)
        
    def on_privmsg(self, connect, event):
        service = self.get_service_name(connect)
        if ''.join(event.arguments()) == self.ident:
            logging.debug('%s::check_online successful' % service)
            try:
                last_online = Setting.byName("last_online")
            except SQLObjectNotFound:
                last_online = Setting(name="last_online", data=None)
            last_online.data = datetime.now()
        else:
            logging.warn('%s::PRIVMSG %s' % (service, ''.join(event.arguments())))
            
    def on_join(self, connect, event):
        service = self.get_service_name(connect)
        if event.source().split('!')[0] == Service.byName(service).nickname+self.alternate:
            logging.info('JOINed %s on %s' % (event.target(), service))
            
    def on_topic(self, connect, event):
        service = self.get_service_name(connect)
        logging.info("%s::CHANGETOPIC %s :: %s" % (service, event.target(), ''.join(event.arguments())))
        chan = event.target().split('#')[1]
        chan = Channel.by_name_and_service(chan, service)
        chan.topic = ''.join(event.arguments())

    def on_currenttopic(self, connect, event):
        service = self.get_service_name(connect)
        chan = Channel.by_name_and_service(event.arguments()[0].split('#')[1], service)
        chan.topic = event.arguments()[1]
        logging.info("%s::CURRENTTOPIC %s :: %s" % (service, event.arguments()[0], event.arguments()[1]))
        
    def on_privnotice(self, connect, event):
        service = self.get_service_name(connect)
        logging.info('%s::PRIVNOTICE %s' % (service, ''.join(event.arguments())))
           
    def on_pubmsg(self, connect, event):
        service = self.get_service_name(connect)
        if event.target().startswith('#') and event.target()[1:] in self.current_channels[service]:
            self.log_message(service, event.source().split('!')[0], event.target()[1:], ''.join(event.arguments()))
        self.check_nick(connect)
        logging.debug('%s::PUBMSG %s' % (service, ''.join(event.arguments())))
        
    def on_action(self, connect, event):
        service = self.get_service_name(connect)
        if event.target().startswith('#') and event.target()[1:] in self.current_channels[service]:
            self.log_message(service, event.source().split('!')[0], event.target()[1:], ''.join(event.arguments()), True)
        self.check_nick(connect)
        
    def on_disconnect(self, connect, event):
        logging.debug("Disconnect called...")
        if not self.shutdown_init:
            service = self.get_service_name(connect)
            try:
                connect.quit("Bot disconnected...")
            except irclib.ServerNotConnectedError:
                pass
            self.connect_service(service)
        
    ###
    # The logging bit...
    ###
    def log_message(self, service, source, target, message, isaction=False):
        logging.debug("starting log_message: %s - %s - %s" % (service, target, source))
        channel = Channel.by_name_and_service(target, service)
        
        try:
            msg = Message(channel=channel, nick=source, line=message, time=datetime.now(), isaction=isaction)
        except UnicodeDecodeError, e:
            logging.info('DECODEERROR %s' % e)
            d_source, enc = self.guess_encoding(source)
            logging.info('ENCODING source = %s' % enc)
            d_message, enc = self.guess_encoding(message)
            logging.info('ENCODING line = %s' % enc)
            msg = Message(channel=channel, nick=d_source, line=d_message, time=datetime.now(), isaction=isaction)
            
    def guess_encoding(self, data):
        """
        Given a byte string, attempt to decode it.
        Tries the standard 'UTF8' and 'latin-1' encodings,
        Plus several gathered from locale information.

        The calling program *must* first call 
            locale.setlocale(locale.LC_ALL, '')

        If successful it returns 
            (decoded_unicode, successful_encoding)
        If unsuccessful it raises a ``UnicodeError``
        """
        successful_encoding = None
        # we make 'utf-8' the first encoding
        encodings = ['utf-8']
        #
        # next we add anything we can learn from the locale
        try:
            encodings.append(locale.nl_langinfo(locale.CODESET))
        except AttributeError:
            pass
        try:
            encodings.append(locale.getlocale()[1])
        except (AttributeError, IndexError):
            pass
        try:
            encodings.append(locale.getdefaultlocale()[1])
        except (AttributeError, IndexError):
            pass
        #
        # we try 'latin-1' last
        encodings.append('latin-1')
        for enc in encodings:
            # some of the locale calls 
            # may have returned None
            if not enc:
                continue
            try:
                decoded = unicode(data, enc)
                successful_encoding = enc

            except (UnicodeError, LookupError):
                pass
            else:
                break
        if not successful_encoding:
             raise UnicodeError(
            'Unable to decode input data.  Tried the following encodings: %s.'
            % ', '.join([repr(enc) for enc in encodings if enc]))
        else:
             return (decoded, successful_encoding)

if __name__ == "__main__":
    _TEST_SERVICES = []
    _TEST_SERVICES.append(dict(service="Freenode", server="irc.freenode.net",
                        port=6667, nick="arkivotest"))
    _TEST_SERVICES.append(dict(service="oftc", server="irc.oftc.net", port=6667, nick="arkivotest"))

    _TEST_CHANNELS = [{"service": "Freenode", "channels": ['arkivotest',]},
                    {"service": "oftc", "channels": ['arkivotest',]}]
                    
    #formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
    #stream = logging.StreamHandler()
    #if logfile is not None:
        #stream = logging.handlers.RotatingFileHandler(logfile, 'a', 1000000, 7)
    #stream.setFormatter(formatter)
    #logging.getLogger().addHandler(stream)
    
    #logging.getLogger().setLevel(logging.DEBUG)
    
    bot = ArkivoBot()
    
    # Set the locale (for guessing the encoding)
    locale.setlocale(locale.LC_ALL, '')
    
    try:
        while 1:
            if bot.connect():
                logging.info('connecting to server')
                try: bot.start()
                except irclib.ServerNotConnectedError: pass
            logging.info('dropped out of server loop')
            time.sleep(delay)
    finally:
        bot.shutdown()
        logging.info('SYSTEM bot shutdown')
        logging.info('Emailing bot owner')
        
        import smtplib
        now = datetime.now()
        
        fromaddr = "arkivo@splee.co.uk"
        toaddrs = ["spleeman+arkivobot@gmail.com"]
        
        msg = ("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n"
                % (fromaddr, ", ".join(toaddrs), "Arkivobot has died!"))
                
        msg = msg + "Our friend, Arkivobot, has passed away.  Please boot him back to life ASAP."
        
        try:
            server = smtplib.SMTP('localhost')
            result = server.sendmail(fromaddr, toaddrs, msg)
            server.quit()
        except socket.error:
            logging.error("Email failed!")
        
        logging.shutdown()
    